Passed
Branch v10.2.x (167edf)
by Rafael S.
02:17
created

WaveFileMetaEditor.deleteCuePoint   A

Complexity

Conditions 4

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 21
rs 9.65
c 0
b 0
f 0
cc 4
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileMetaEditor class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { WaveFileCreator } from './wavefile-creator';
31
32
/**
33
 * A class to edit meta information in wav files.
34
 * @extends WaveFileCreator
35
 * @ignore
36
 */
37
export class WaveFileMetaEditor extends WaveFileCreator {
38
39
  /**
40
   * Return the value of a RIFF tag in the INFO chunk.
41
   * @param {string} tag The tag name.
42
   * @return {?string} The value if the tag is found, null otherwise.
43
   */
44
  getTag(tag) {
45
    /** @type {!Object} */
46
    let index = this.getTagIndex_(tag);
47
    if (index.TAG !== null) {
48
      return this.LIST[index.LIST].subChunks[index.TAG].value;
49
    }
50
    return null;
51
  }
52
53
  /**
54
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
55
   * then it is created. It if exists, it is overwritten.
56
   * @param {string} tag The tag name.
57
   * @param {string} value The tag value.
58
   * @throws {Error} If the tag name is not valid.
59
   */
60
  setTag(tag, value) {
61
    tag = fixRIFFTag_(tag);
62
    /** @type {!Object} */
63
    let index = this.getTagIndex_(tag);
64
    if (index.TAG !== null) {
65
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
66
        value.length + 1;
67
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
68
    } else if (index.LIST !== null) {
69
      this.LIST[index.LIST].subChunks.push({
70
        chunkId: tag,
71
        chunkSize: value.length + 1,
72
        value: value});
73
    } else {
74
      this.LIST.push({
75
        chunkId: 'LIST',
76
        chunkSize: 8 + value.length + 1,
77
        format: 'INFO',
78
        subChunks: []});
79
      this.LIST[this.LIST.length - 1].subChunks.push({
80
        chunkId: tag,
81
        chunkSize: value.length + 1,
82
        value: value});
83
    }
84
  }
85
86
  /**
87
   * Remove a RIFF tag from the INFO chunk.
88
   * @param {string} tag The tag name.
89
   * @return {boolean} True if a tag was deleted.
90
   */
91
  deleteTag(tag) {
92
    /** @type {!Object} */
93
    let index = this.getTagIndex_(tag);
94
    if (index.TAG !== null) {
95
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
96
      return true;
97
    }
98
    return false;
99
  }
100
101
  /**
102
   * Return a Object<tag, value> with the RIFF tags in the file.
103
   * @return {!Object<string, string>} The file tags.
104
   */
105
  listTags() {
106
    /** @type {?number} */
107
    let index = this.getLISTINFOIndex_();
108
    /** @type {!Object} */
109
    let tags = {};
110
    if (index !== null) {
111
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
112
        tags[this.LIST[index].subChunks[i].chunkId] =
113
          this.LIST[index].subChunks[i].value;
114
      }
115
    }
116
    return tags;
117
  }
118
119
  /**
120
   * Return an array with all cue points in the file, in the order they appear
121
   * in the file.
122
   * Objects representing cue points/regions look like this:
123
   *   {
124
   *     position: 500, // the position in milliseconds
125
   *     label: 'cue marker 1',
126
   *     end: 1500, // the end position in milliseconds
127
   *     dwName: 1,
128
   *     dwPosition: 0,
129
   *     fccChunk: 'data',
130
   *     dwChunkStart: 0,
131
   *     dwBlockStart: 0,
132
   *     dwSampleOffset: 22050, // the position as a sample offset
133
   *     dwSampleLength: 3646827, // length as a sample count, 0 if not a region
134
   *     dwPurposeID: 544106354,
135
   *     dwCountry: 0,
136
   *     dwLanguage: 0,
137
   *     dwDialect: 0,
138
   *     dwCodePage: 0,
139
   *   }
140
   * @return {!Array<Object>}
141
   */
142
  listCuePoints() {
143
    /** @type {!Array<!Object>} */
144
    let points = this.getCuePoints_();
145
    for (let i = 0, len = points.length; i < len; i++) {
146
147
      // Add attrs that should exist in the object
148
      points[i].position =
149
        (points[i].dwSampleOffset / this.fmt.sampleRate) * 1000;
150
151
      // If it is a region, calc the end
152
      // position in milliseconds
153
      if (points[i].dwSampleLength) {
154
        points[i].end =
155
          (points[i].dwSampleLength / this.fmt.sampleRate) * 1000;
156
        points[i].end += points[i].position;
157
      // If its not a region, end should be null
158
      } else {
159
        points[i].end = null;
160
      }
161
162
      // Remove attrs that should not go in the results
163
      delete points[i].value;
164
    }
165
    return points;
166
  }
167
168
  /**
169
   * Create a cue point in the wave file.
170
   * @param {!{
171
   *   position: number,
172
   *   label: ?string,
173
   *   end: ?number,
174
   *   dwPurposeID: ?number,
175
   *   dwCountry: ?number,
176
   *   dwLanguage: ?number,
177
   *   dwDialect: ?number,
178
   *   dwCodePage: ?number
179
   * }} pointData A object with the data of the cue point.
180
   *
181
   * # Only required attribute to create a cue point:
182
   * pointData.position: The position of the point in milliseconds
183
   *
184
   * # Optional attribute for cue points:
185
   * pointData.label: A string label for the cue point
186
   *
187
   * # Extra data used for regions
188
   * pointData.end: A number representing the end of the region,
189
   *   in milliseconds, counting from the start of the file. If
190
   *   no end attr is specified then no region is created.
191
   *
192
   * # You may also specify the following attrs for regions, all optional:
193
   * pointData.dwPurposeID
194
   * pointData.dwCountry
195
   * pointData.dwLanguage
196
   * pointData.dwDialect
197
   * pointData.dwCodePage
198
   */
199
  setCuePoint(pointData) {
200
    this.cue.chunkId = 'cue ';
201
202
    // label attr should always exist
203
    if (!pointData.label) {
204
      pointData.label = '';
205
    }
206
207
    /**
208
     * Load the existing points before erasing
209
     * the LIST 'adtl' chunk and the cue attr
210
     * @type {!Array<!Object>}
211
     */
212
    let existingPoints = this.getCuePoints_();
213
214
    // Clear any LIST labeled 'adtl'
215
    // The LIST chunk should be re-written
216
    // after the new cue point is created
217
    this.clearLISTadtl_();
218
219
    // Erase this.cue so it can be re-written
220
    // after the point is added
221
    this.cue.points = [];
222
223
    /**
224
     * Cue position param is informed in milliseconds,
225
     * here its value is converted to the sample offset
226
     * @type {number}
227
     */
228
    pointData.dwSampleOffset =
229
      (pointData.position * this.fmt.sampleRate) / 1000;
230
    /**
231
     * end param is informed in milliseconds, counting
232
     * from the start of the file.
233
     * here its value is converted to the sample length
234
     * of the region.
235
     * @type {number}
236
     */
237
    pointData.dwSampleLength = 0;
238
    if (pointData.end) {
239
      pointData.dwSampleLength = 
240
        ((pointData.end * this.fmt.sampleRate) / 1000) -
241
        pointData.dwSampleOffset;
242
    }
243
244
    // If there were no cue points in the file,
245
    // insert the new cue point as the first
246
    if (existingPoints.length === 0) {
247
      this.setCuePoint_(pointData, 1);
248
249
    // If the file already had cue points, This new one
250
    // must be added in the list according to its position.
251
    } else {
252
      this.setCuePointInOrder_(existingPoints, pointData);
253
    }
254
    this.cue.dwCuePoints = this.cue.points.length;
255
  }
256
257
  /**
258
   * Remove a cue point from a wave file.
259
   * @param {number} index the index of the point. First is 1,
260
   *    second is 2, and so on.
261
   */
262
  deleteCuePoint(index) {
263
    this.cue.chunkId = 'cue ';
264
    /** @type {!Array<!Object>} */
265
    let existingPoints = this.getCuePoints_();
266
    this.clearLISTadtl_();
267
    /** @type {number} */
268
    let len = this.cue.points.length;
269
    this.cue.points = [];
270
    for (let i = 0; i < len; i++) {
271
      if (i + 1 !== index) {
272
        this.setCuePoint_(existingPoints[i], i + 1);
273
      }
274
    }
275
    this.cue.dwCuePoints = this.cue.points.length;
276
    if (this.cue.dwCuePoints) {
277
      this.cue.chunkId = 'cue ';
278
    } else {
279
      this.cue.chunkId = '';
280
      this.clearLISTadtl_();
281
    }
282
  }
283
284
  /**
285
   * Update the label of a cue point.
286
   * @param {number} pointIndex The ID of the cue point.
287
   * @param {string} label The new text for the label.
288
   */
289
  updateLabel(pointIndex, label) {
290
    /** @type {?number} */
291
    let cIndex = this.getAdtlChunk_();
292
    if (cIndex !== null) {
293
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
294
        if (this.LIST[cIndex].subChunks[i].dwName ==
295
            pointIndex) {
296
          this.LIST[cIndex].subChunks[i].value = label;
297
        }
298
      }
299
    }
300
  }
301
302
  /**
303
   * Return an array with all cue points in the file, in the order they appear
304
   * in the file.
305
   * @return {!Array<!Object>}
306
   * @private
307
   */
308
  getCuePoints_() {
309
    /** @type {!Array<!Object>} */
310
    let points = [];
311
    for (let i = 0; i < this.cue.points.length; i++) {
312
      /** @type {!Object} */
313
      let chunk = this.cue.points[i];
314
      /** @type {!Object} */
315
      let pointData = this.getDataForCuePoint_(chunk.dwName);
316
      pointData.label = pointData.value ? pointData.value : '';
317
      pointData.dwPosition = chunk.dwPosition;
318
      pointData.fccChunk = chunk.fccChunk;
319
      pointData.dwChunkStart = chunk.dwChunkStart;
320
      pointData.dwBlockStart = chunk.dwBlockStart;
321
      pointData.dwSampleOffset = chunk.dwSampleOffset;
322
      points.push(pointData);
323
    }
324
    return points;
325
  }
326
327
  /**
328
   * Return the associated data of a cue point.
329
   * @param {number} pointDwName The ID of the cue point.
330
   * @return {!Object}
331
   * @private
332
   */
333
  getDataForCuePoint_(pointDwName) {
334
    /** @type {?number} */
335
    let LISTindex = this.getAdtlChunk_();
336
    /** @type {!Object} */
337
    let pointData = {};
338
    // If there is a adtl LIST in the file, look for
339
    // LIST subchunks with data referencing this point
340
    if (LISTindex !== null) {
341
      this.getCueDataFromLIST_(pointData, LISTindex, pointDwName);
342
    }
343
    return pointData;
344
  }
345
346
  /**
347
   * Get all data associated to a cue point in a LIST chunk.
348
   * @param {!Object} pointData A object to hold the point data.
349
   * @param {number} index The index of the adtl LIST chunk.
350
   * @param {number} pointDwName The ID of the cue point.
351
   * @private
352
   */
353
  getCueDataFromLIST_(pointData, index, pointDwName) {
354
    // got through all chunks in the adtl LIST checking
355
    // for references to this cue point
356
    for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
357
      if (this.LIST[index].subChunks[i].dwName == pointDwName) {
358
        /** @type {!Object} */
359
        let chunk = this.LIST[index].subChunks[i];
360
        // Some chunks may reference the point but
361
        // have a empty text; this is to ensure that if
362
        // one chunk that reference the point has a text,
363
        // this value will be kept as the associated data label
364
        // for the cue point.
365
        // If different values are present, the last value found
366
        // will be considered the label for the cue point.
367
        pointData.value = chunk.value || pointData.value;
368
        pointData.dwName = chunk.dwName || 0;
369
        pointData.dwSampleLength = chunk.dwSampleLength || 0;
370
        pointData.dwPurposeID = chunk.dwPurposeID || 0;
371
        pointData.dwCountry = chunk.dwCountry || 0;
372
        pointData.dwLanguage = chunk.dwLanguage || 0;
373
        pointData.dwDialect = chunk.dwDialect || 0;
374
        pointData.dwCodePage = chunk.dwCodePage || 0;
375
      }
376
    }
377
  }
378
379
  /**
380
   * Return the index of the INFO chunk in the LIST chunk.
381
   * @return {?number} the index of the INFO chunk.
382
   * @private
383
   */
384
  getLISTINFOIndex_() {
385
    /** @type {?number} */
386
    let index = null;
387
    for (let i = 0, len = this.LIST.length; i < len; i++) {
388
      if (this.LIST[i].format === 'INFO') {
389
        index = i;
390
        break;
391
      }
392
    }
393
    return index;
394
  }
395
396
  /**
397
   * Return the index of the 'adtl' LIST in this.LIST.
398
   * @return {?number}
399
   * @private
400
   */
401
  getAdtlChunk_() {
402
    for (let i = 0, len = this.LIST.length; i < len; i++) {
403
      if (this.LIST[i].format == 'adtl') {
404
        return i;
405
      }
406
    }
407
    return null;
408
  }
409
410
  /**
411
   * Return the index of a tag in a FILE chunk.
412
   * @param {string} tag The tag name.
413
   * @return {!Object<string, ?number>}
414
   *    Object.LIST is the INFO index in LIST
415
   *    Object.TAG is the tag index in the INFO
416
   * @private
417
   */
418
  getTagIndex_(tag) {
419
    /** @type {!Object<string, ?number>} */
420
    let index = {LIST: null, TAG: null};
421
    for (let i = 0, len = this.LIST.length; i < len; i++) {
422
      if (this.LIST[i].format == 'INFO') {
423
        index.LIST = i;
424
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
425
          if (this.LIST[i].subChunks[j].chunkId == tag) {
426
            index.TAG = j;
427
            break;
428
          }
429
        }
430
        break;
431
      }
432
    }
433
    return index;
434
  }
435
436
  /**
437
   * Push a new cue point in this.cue.points.
438
   * @param {!Object} pointData A object with data of the cue point.
439
   * @param {number} dwName the dwName of the cue point
440
   * @private
441
   */
442
  setCuePoint_(pointData, dwName) {
443
    this.cue.points.push({
444
      dwName: dwName,
445
      dwPosition: pointData.dwPosition ? pointData.dwPosition : 0,
446
      fccChunk: pointData.fccChunk ? pointData.fccChunk : 'data',
447
      dwChunkStart: pointData.dwChunkStart ? pointData.dwChunkStart : 0,
448
      dwBlockStart: pointData.dwBlockStart ? pointData.dwBlockStart : 0,
449
      dwSampleOffset: pointData.dwSampleOffset
450
    });
451
    this.setLabl_(pointData, dwName);
452
  }
453
454
  /**
455
   * Push a new cue point in this.cue.points according to existing cue points.
456
   * @param {!Array} existingPoints Array with the existing points.
457
   * @param {!Object} pointData A object with data of the cue point.
458
   * @private
459
   */
460
  setCuePointInOrder_(existingPoints, pointData) {
461
    /** @type {boolean} */
462
    let hasSet = false;
463
464
    // Iterate over the cue points that existed
465
    // before this one was added
466
    for (let i = 0; i < existingPoints.length; i++) {
467
468
      // If the new point is located before this original point
469
      // and the new point have not been created, create the
470
      // new point and then the original point
471
      if (existingPoints[i].dwSampleOffset > 
472
        pointData.dwSampleOffset && !hasSet) {
473
        // create the new point
474
        this.setCuePoint_(pointData, i + 1);
475
476
        // create the original point
477
        this.setCuePoint_(existingPoints[i], i + 2);
478
        hasSet = true;
479
480
      // Otherwise, re-create the original point
481
      } else {
482
        this.setCuePoint_(existingPoints[i], hasSet ? i + 2 : i + 1);
483
      }
484
    }
485
    // If no point was created in the above loop,
486
    // create the new point as the last one
487
    if (!hasSet) {
488
      this.setCuePoint_(pointData, this.cue.points.length + 1);
489
    }
490
  }
491
492
  /**
493
   * Clear any LIST chunk labeled as 'adtl'.
494
   * @private
495
   */
496
  clearLISTadtl_() {
497
    for (let i = 0, len = this.LIST.length; i < len; i++) {
498
      if (this.LIST[i].format == 'adtl') {
499
        this.LIST.splice(i);
500
      }
501
    }
502
  }
503
504
  /**
505
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
506
   * This method creates a LIST adtl chunk in the file if one
507
   * is not present.
508
   * @param {!Object} pointData A object with data of the cue point.
509
   * @param {number} dwName The ID of the cue point.
510
   * @private
511
   */
512
  setLabl_(pointData, dwName) {
513
    /**
514
     * Get the index of the LIST chunk labeled as adtl.
515
     * A file can have many LIST chunks with unique labels.
516
     * @type {?number}
517
     */
518
    let adtlIndex = this.getAdtlChunk_();
519
    // If there is no adtl LIST, create one
520
    if (adtlIndex === null) {
521
      // Include a new item LIST chunk
522
      this.LIST.push({
523
        chunkId: 'LIST',
524
        chunkSize: 4,
525
        format: 'adtl',
526
        subChunks: []});
527
      // Get the index of the new LIST chunk
528
      adtlIndex = this.LIST.length - 1;
529
    }
530
    this.setLabelText_(adtlIndex, pointData, dwName);
531
    if (pointData.dwSampleLength) {
532
      this.setLtxtChunk_(adtlIndex, pointData, dwName);
533
    }
534
  }
535
536
  /**
537
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
538
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
539
   * @param {!Object} pointData A object with data of the cue point.
540
   * @param {number} dwName The ID of the cue point.
541
   * @private
542
   */
543
  setLabelText_(adtlIndex, pointData, dwName) {
544
    this.LIST[adtlIndex].subChunks.push({
545
      chunkId: 'labl',
546
      chunkSize: 4, // should be 4 + label length in bytes
547
      dwName: dwName,
548
      value: pointData.label
549
    });
550
    this.LIST[adtlIndex].chunkSize += 12; // should be 4 + label byte length
551
  }
552
  /**
553
   * Create a new 'ltxt' subchunk in a 'LIST' chunk of type 'adtl'.
554
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
555
   * @param {!Object} pointData A object with data of the cue point.
556
   * @param {number} dwName The ID of the cue point.
557
   * @private
558
   */
559
  setLtxtChunk_(adtlIndex, pointData, dwName) {
560
    this.LIST[adtlIndex].subChunks.push({
561
      chunkId: 'ltxt',
562
      chunkSize: 20,  // should be 12 + label byte length
563
      dwName: dwName,
564
      dwSampleLength: pointData.dwSampleLength,
565
      dwPurposeID: pointData.dwPurposeID ? pointData.dwPurposeID : 0,
566
      dwCountry: pointData.dwCountry ? pointData.dwCountry : 0,
567
      dwLanguage: pointData.dwLanguage ? pointData.dwLanguage : 0,
568
      dwDialect: pointData.dwDialect ? pointData.dwDialect : 0,
569
      dwCodePage: pointData.dwCodePage ? pointData.dwCodePage : 0,
570
      value: pointData.label // kept for compatibility
571
    });
572
    this.LIST[adtlIndex].chunkSize += 28;
573
  }
574
}
575
576
/**
577
 * Fix a RIFF tag format if possible, throw an error otherwise.
578
 * @param {string} tag The tag name.
579
 * @return {string} The tag name in proper fourCC format.
580
 * @private
581
 */
582
function fixRIFFTag_(tag) {
583
  if (tag.constructor !== String) {
584
    throw new Error('Invalid tag name.');
585
  } else if (tag.length < 4) {
586
    for (let i = 0, len = 4 - tag.length; i < len; i++) {
587
      tag += ' ';
588
    }
589
  }
590
  return tag;
591
}
592